package com.wk.portal.aspect;

import com.alibaba.fastjson.JSON;
import com.wk.portal.annotion.GmallCache;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

/**
 * @GmallCache 注解的切面类
 */

@Component      //声明该类是一个组件
@Aspect         //声明该类是一个切面
public class CacheAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 1.方法的返回值必须是object
     * 2.方法的参数proceedingJoinPoint
     * 3.方法必须抛出Throwable异常
     * 4.proceedingJoinPoint.proceed(args)执行业务方法

     * @annotation : 切入点表达式，表示标注了某个注解的所有方法。对有@GmallCache注解的所有方法增加环绕通知
     */
    @Around("@annotation(com.wk.portal.annotion.GmallCache)")
    public Object cacheAround(ProceedingJoinPoint joinPoint) throws Throwable{

        Object result;

        //-----------------------------------前置方法开始-----------------------------------------

        //获取目标方法
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        Method method = signature.getMethod();

        //获取目标方法的注解
        GmallCache gmallCache = method.getAnnotation(GmallCache.class);

        //获取目标方法返回值
        Class<?> returnType = method.getReturnType();

        //从注解对象中获取Redis前缀
        String redisPrefix = gmallCache.value();

        //从注解对象中获取Redis过期时间
        int timeout = gmallCache.timeout();

        //从注解对象中获取过期时间基础上的随机时间
        int random = gmallCache.random();

        //获取目标方法的参数
        Object[] args = joinPoint.getArgs();

        //获取Redis的key
        String arg = Arrays.asList(args).get(0).toString();
        String key = redisPrefix+arg;

        //从缓存中查询对应的数据 如果命中则直接返回
        result =  cacheHit(key, returnType);
        if (result != null) {
            return result;
        }

        /*没有命中,先加分布式锁
        分布式锁 只锁定操作对象 避免击穿现象*/
        RLock lock = redissonClient.getLock("cacheLock:" + arg);
        lock.lock();

        //再次查询缓存, 有数据则返回, 没有数据则执行目标方法
        result =  cacheHit(key, returnType);
        if (result != null) {
            lock.unlock();      //如果数据不为空，则返回数据并释放分布式锁
            return result;
        }

        //-----------------------------------前置方法结束-----------------------------------------

        //执行目标业务方法
        result = joinPoint.proceed(args);       //通过目标参数执行目标方法

        //-----------------------------------后置方法开始-----------------------------------------

        //将查询结果转为json后放入缓存 并添加过期时间
        redisTemplate.boundValueOps(key).set(JSON.toJSONString(result),timeout+Math.round(random), TimeUnit.MINUTES);

        //返回之前释放锁
        lock.unlock();

        //-----------------------------------后置方法结束-----------------------------------------

        return result;
    }

    private Object cacheHit(String key, Class<?> returnType) {
        String json = redisTemplate.boundValueOps(key).get();

        //缓存命中则直接返回
        if(StringUtils.isNotBlank(json)){
            return JSON.parseObject(json, returnType);
        }

        //缓存没有命中返回null
        return null;
    }

}
